Skip to content

feat(browser): server-side traced, /server and Next.js entries - #1128

Open
JeremyFunk wants to merge 4 commits into
mainfrom
feat/browser-server-nextjs
Open

JeremyFunk wants to merge 4 commits into
mainfrom
feat/browser-server-nextjs

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Server-side helpers and a Next.js integration for @maple-dev/browser, replacing the glue the frontend guides had customers copy.

Fix: traced on the server. MapleBrowser.traced was a no-op without window, but guides use it for server-side data loading (Server Components, SSR loaders, SSR resolvers). On the server it now runs fn in a span from the global tracer, under the active context, so it nests under whatever the app registered (@vercel/otel, NodeSDK) and keeps its parent across await. It doesn't touch navigation state. Without server OTel it only runs fn.

New entries (the main entry's bundle is unchanged apart from the server traced branch: eager 37.39 → 37.42 kB, first-party 14.25 → 14.30 kB, budgets untouched):

Export Purpose
@maple-dev/browser/server → traced(name, fn, options?) Server data loading in a span under the active one; same function the main entry uses on the server
@maple-dev/browser/server → serverTiming(): string | undefined traceparent;desc="00-…" for the active span (sampled flag kept), for any framework's response hook
@maple-dev/browser/nextjs → onRouterTransitionStart(url) Re-exported from instrumentation-client.ts: starts a span per App Router navigation
@maple-dev/browser/nextjs → MapleNavigation "use client" component for the root layout; wraps its own <Suspense>, ends the span named after the route (/projects/[id], /[...slug], /_not-found)
@maple-dev/browser/nextjs → reportNextError(error) For error.tsx / global-error.tsx: skips digest errors, dedupes
@maple-dev/browser/nextjs/server → withMapleProxy(proxy?) Wraps or creates proxy.ts/middleware.ts: request traceparent + response Server-Timing, composes with your proxy

/server and /nextjs/server depend only on @opentelemetry/api (+ next/server), so they're safe in Node, edge runtimes and Workers. next and react are optional peers (*, so no existing install gets a peer conflict).

Behaviour notes

  • Error dedupe stays once per error object (WeakSet), shared by traced, captureException and the global handlers, on the server too. Scoping it per trace was tried and dropped: the browser loses the trace at every await, so an error re-entering from another trace was recorded twice, and concurrent requests raced on it. The trade-off: an error object shared by requests (a memoized promise's rejection) is recorded by traced on the first request only. The framework's own spans still record the rest.
  • withMapleProxy only changes responses that go on to a render in this app (next(), same-origin rewrite()). Redirects, your own responses (including immutable ones) and external rewrites pass through unchanged. With no user proxy it uses NextResponse.next({ request: { headers } }). Composing with a user's next()/rewrite() extends the same override headers the way Next.js extends them for its router headers. A traceparent the render already receives is kept (a user's next({ request }) that drops it gets ours instead). Only a document request (Sec-Fetch-Dest: document, e.g. a load balancer's traceparent) still gets Server-Timing, so client-navigation RSC responses stay untouched.
  • MapleNavigation keys its effect on pathname, query and route template, not the useParams() object, so router.refresh() or a revalidating server action doesn't end a navigation in flight. A link back to the route on screen ends an in-flight navigation as interrupted.

Verification

  • packages/browser: typecheck, 143 tests (node + Chromium, including a React test for MapleNavigation and proxy tests against the real next/server), build ("use client" stays at the top of dist/nextjs.mjs), size budget.
  • End to end: a Next.js 16.3 app instrumented with the old hand-written guide code was switched to the packed tarball (next build && next start), then driven with Playwright across 21 scenarios. Browser spans match the old run: pageload/navigate naming, /_not-found, interrupted, redirect, back/forward, query/hash, digest skip, render/loader error counts, propagation. The page load joins middleware GET → render → server traced span → server fetch after an await → API. MapleBrowser.traced from the main entry in a Server Component nests under RSC GET /slow. One difference: a <Link> to an unmatched URL (full reload) now exports the old document's navigate span as interrupted instead of dropping it. That's the SDK's existing pagehide handling, not new here.
  • Proxy composition checked against real Next.js: the render's traceparent header equals the Server-Timing one for no proxy, a user next({ request }), a plain next() and a same-origin rewrite(). User request headers are kept. Redirects are untouched.

Known limits

  • basePath: Next.js passes back/forward URLs with the basePath but push URLs without it. A back/forward to a hash entry can open a span that ends as interrupted.
  • Route templates are rebuilt from params by matching from the end. A static segment after a param with the same value (/users/settings/settings for /users/[name]/settings) is ambiguous without the route tree.
  • Optional catch-all routes: useParams() can't tell [[...slug]] from [...slug], so /docs is navigate /docs and /docs/a is navigate /docs/[...slug].
  • A root-layout error that renders global-error.tsx unmounts MapleNavigation, so that navigation ends as interrupted at the next navigation or when the page is left.
  • The browser follows the server's sampling decision (documented).

Summary by CodeRabbit

  • New Features
    • Added server-side tracing to connect data-loading spans with browser page loads.
    • Added Next.js App Router support for navigation tracing, error reporting, and trace propagation through proxies.
  • Documentation
    • Expanded integration guidance for server tracing and Next.js, and clarified when browser navigation tracing runs.

- traced spans through the global tracer on the server (was a no-op), so
  Server Components and SSR loaders nest under the server's own spans
- @maple-dev/browser/server: traced and serverTiming(), @opentelemetry/api only
- @maple-dev/browser/nextjs: onRouterTransitionStart, MapleNavigation,
  reportNextError; /nextjs/server: withMapleProxy
- errors are recorded once per trace instead of once per process
- dedupe errors once per object again: per trace re-recorded errors that
  crossed an await in the browser, and raced across concurrent requests
- withMapleProxy: build next() through the public API when there is no user
  proxy; tell the browser on a page load whose traceparent a load balancer added
- MapleNavigation: key the effect on the route string, so a same-route commit
  (router.refresh, server action) doesn't end a navigation in flight
- routeTemplate: match partly encoded segments by decoding them
@maple-review-bot

maple-review-bot Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 4/5 · likely safe to merge
traced changes behaviour on the server for an existing public export, but it is guarded by typeof window, reuses the extracted runTraced/recordFailure, and both paths are tested.
quality 100/100 · no findings · tests covered · risk medium · 4/4 new units observable

Adds server-side traced/serverTiming, @maple-dev/browser/nextjs (navigation, error reporting, proxy wrapper) and extracts failure recording into failures.ts. Behaviour on the server is newly spanned only when the app registered global OTel, and the browser path is unchanged. Safe to merge.

  • MapleBrowser.traced runs in a global-tracer span on the server (navigation.ts:85, server.ts:19)
  • New entries ./server and ./nextjs/./nextjs/server in the exports map (package.json:34)
  • withMapleProxy adds request traceparent and response Server-Timing (nextjs/server.ts:23)
  • alreadyReported/recordFailure/runTraced moved to failures.ts
What was checked
  • routeTemplate against percent-encoded, malformed-escape, catch-all and trailing-slash paths — all handled by the end-anchored splice (nextjs/route.ts:14), cases in route.test.ts:5
  • Error dedupe is one WeakSet shared by traced, captureException and the global handlers; the cross-request consequence is documented and pinned by server.test.ts:253
  • withMapleProxy never touches redirects, foreign-origin rewrites or immutable responses — rendersHere gates the three header writes (nextjs/server.ts:55), tests nextjs/server.test.ts:186
Observability coverage: 4 of 4 changes observable
Change Kind Observable Evidence
server traced data-loading span span yes server.ts:21 starts an active span on the global tracer, parent from context.active()
serverTiming() response hook trace propagation yes server.ts:31 emits traceparent;desc=… from the active span (traceparent.ts:23)
App Router navigation spans (onRouterTransitionStart + MapleNavigation) span yes navigation.ts:68 pageload/navigate span, ended and renamed by nextjs/index.ts:44
withMapleProxy middleware wrapper span yes runs inside Next.js's own middleware span on the active context (nextjs/server.ts:4), verified in nextjs/server.test.ts:57

5da70e4 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: d055036f-6c35-4174-8564-108040cbcc73

📥 Commits

Reviewing files that changed from the base of the PR and between 5da70e4 and 3c87b10.

📒 Files selected for processing (2)
  • packages/browser/src/nextjs/server.test.ts
  • packages/browser/src/nextjs/server.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.


📝 Walkthrough

Walkthrough

The browser package adds server-side tracing APIs and Next.js App Router integrations for navigation spans, error reporting, and trace propagation between proxy requests and server renders. Package exports, tests, and documentation are updated for these additions.

Changes

Browser package tracing integrations

Layer / File(s) Summary
Shared failure handling and server tracing
packages/browser/src/failures.ts, packages/browser/src/server.ts, packages/browser/src/traceparent.ts, packages/browser/src/navigation.ts, packages/browser/src/errors.ts, packages/browser/src/index.ts, packages/browser/src/server.test.ts, packages/browser/src/errors.browser.test.ts, packages/browser/src/navigation.browser.test.ts, packages/browser/README.md, apps/landing/src/content/docs/session-replay/browser-sdk.md
Shared failure handling is used by browser and server tracing. The server entry point adds traced and serverTiming; the documentation describes server tracing behavior and its interaction with browser page loads.
Next.js client navigation and error reporting
packages/browser/src/nextjs/route.ts, packages/browser/src/nextjs/index.ts, packages/browser/src/nextjs/route.test.ts, packages/browser/src/nextjs/index.browser.test.ts, packages/browser/package.json, packages/browser/tsdown.config.ts, knip.json, packages/browser/README.md, apps/landing/src/content/docs/session-replay/browser-sdk.md
The client integration adds navigation span lifecycle handling, route-template names, and client error reporting. Package exports, build and Knip entries, dependencies, tests, and setup documentation cover the integration.
Next.js proxy trace propagation
packages/browser/src/nextjs/server.ts, packages/browser/src/nextjs/server.test.ts, packages/browser/package.json, packages/browser/tsdown.config.ts, packages/browser/README.md, apps/landing/src/content/docs/session-replay/browser-sdk.md
The proxy wrapper forwards trace context to app renders and adds Server-Timing to rendered responses. Tests and documentation cover proxy response handling and trace propagation.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant withMapleProxy
  participant Proxy
  participant AppRender
  Browser->>withMapleProxy: send request
  withMapleProxy->>Proxy: invoke proxy
  Proxy-->>withMapleProxy: return proxy response
  withMapleProxy->>AppRender: forward trace context for app render
  AppRender-->>withMapleProxy: return rendered response
  withMapleProxy-->>Browser: return response with Server-Timing
Loading

Merge Risk: ⚪ Minimal · up to 3c87b

No actionable issue is established; the change is mergeable after normal checks.

Architecture Summary

Architecture risk: 🟡 Medium · up to 3c87b

The change affects 3 systems.

Changed systems: packages/browser, apps/landing, knip.json

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — packages/browser (library) was modified; 18 changed files map to changed impact.
  • observed — apps/landing (service) was modified; 1 changed file maps to changed impact.
  • observed — knip.json (service) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in apps/landing/src/content/docs/session-replay/browser-sdk.md: The browser-only no-op conditions now apply before initialization, with tracing disabled, or before consent; server behavior is documented separately. The existing pre-consent page-load, interrupted-navigation export, and traced fallback details remain.
  • observed — Modified behavior in apps/landing/src/content/docs/session-replay/browser-sdk.md: Documents that server-side startNavigation and endNavigation are no-ops, while traced follows the server-specific behavior.
  • observed — Modified behavior in apps/landing/src/content/docs/session-replay/browser-sdk.md: Adds server-side guidance for @maple-dev/browser/server: traced creates a span under the active server span and preserves parent context across awaits when server OpenTelemetry is registered; without it, the function only runs fn. serverTiming() returns the active span’s traceparent value or undefined, enabling browser page-load trace joining, and notes shared-cache and sampling constraints.
  • observed — Modified behavior in apps/landing/src/content/docs/session-replay/browser-sdk.md: Adds the Next.js App Router integration, including client navigation setup, root-layout navigation completion, client error reporting, and proxy-based server trace propagation. It documents route-template span names, navigation exclusions, error cases skipped by reportNextError, proxy pass-through conditions, shared-cache cautions, and Server Component traced usage.

Reliability and maintainability

  • inferred — Risk-relevant change factors for packages/browser: blast_radius_1; direct_dependents_1
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 85.19% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 16 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: server-side tracing and the new /server and Next.js entry points.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Devin Review

Comment thread packages/browser/src/nextjs/server.ts Outdated
Comment on lines +33 to +37
const carried = request.headers.has("traceparent")
if (carried && request.headers.get("sec-fetch-dest") !== "document") return response
const result =
response ?? NextResponse.next(carried ? undefined : withTraceparent(request, traceparent))
if (response && !carried) forwardToRender(response, request, traceparent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Document render loses its incoming trace

When a proxy override drops an incoming traceparent, withMapleProxy treats the original request header as preserved. The render loses its parent while Server-Timing still joins the browser to the middleware trace.

Learn more

Next.js request-header overrides replace the headers passed to the render. A wrapped proxy can return NextResponse.next({ request: { headers } }) with a filtered header set. Here, carried checks the original request, not the override that the render receives. When that override omits traceparent, the wrapper skips forwarding it but still appends the middleware span to Server-Timing for a document request. The browser joins the middleware trace, while the render does not.

Example: A document request carries traceparent: 00-.... A tenant proxy forwards only x-tenant through NextResponse.next({ request: { headers: new Headers({ 'x-tenant': 'acme' }) } }). The HTML response advertises the middleware trace, but the render sees no traceparent.

Recommended fix: Determine whether the effective request headers delivered by NextResponse.next or a same-origin rewrite retain traceparent. Add the original value to the override when absent, without replacing an existing upstream value; preserve the proxy's other overridden headers.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +20 to +29
// An optional catch-all without segments has nothing to replace
if (!value?.length) continue
const parts: readonly string[] = typeof value === "string" ? [value] : value
const matchesAt = (at: number) =>
parts.every((part, i) => segments[at + i] === part || decode(segments[at + i]) === part)
let at = end - parts.length
while (at > 0 && !matchesAt(at)) at--
// Index 0 is the empty segment before the leading `/`
if (at <= 0) continue
segments.splice(at, parts.length, typeof value === "string" ? `[${name}]` : `[...${name}]`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Optional catch-all route spans split

For /docs/[[...slug]], routeTemplate names /docs as /docs and /docs/a as /docs/[...slug]. The same route splits across span names, so route-level traces cannot group together.

Learn more

Next.js optional catch-all routes match both the base path and paths with one or more additional segments. useParams() supplies no slug for the base path, so skipping empty values leaves the base URL as the span name. For populated values, the current replacement uses the required catch-all form instead of the optional form. useParams() alone does not distinguish required and optional catch-all definitions when values are present.

Example: Visiting /docs under app/docs/[[...slug]]/page.tsx records pageload /docs; visiting /docs/install records navigate /docs/[...slug]. Both pages matched /docs/[[...slug]].

Recommended fix: Obtain route-pattern metadata from the Next.js route configuration or an integration-provided mapping if exact optional catch-all templates are required; otherwise document and normalize the grouping limitation explicitly rather than presenting both names as the matched route template.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +62 to +66
export function reportNextError(error: unknown): void {
// In production a Server Component's error reaches the browser with its
// message stripped and a `digest` added: one meaningless issue for all of them
if (typeof error === "object" && error !== null && "digest" in error && error.digest) return
captureException(error, { name: "react.render_error" })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Global errors leave navigation spans open

When global-error.tsx replaces the root layout, MapleNavigation cannot finish the pending navigation. reportNextError records the error but leaves that span open until another navigation or page exit.

Learn more

The root layout normally renders MapleNavigation, whose effect ends an open navigation after the new route commits. Next.js renders global-error.tsx instead of the root layout for a root-level failure, so that effect does not run. The documented global error boundary calls reportNextError, which records the exception but never closes the navigation. The open span continues to parent later traced work until another navigation or pagehide interrupts it.

Example: A click to /settings starts a navigate span, then the root layout throws and global-error.tsx renders. Reporting that error records react.render_error, but the /settings navigation remains open while the error UI is shown.

Recommended fix: Have the global error integration explicitly close or interrupt the pending navigation when it reports the error, while avoiding changes to ordinary error.tsx handling where the layout and its navigation component can still commit.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@maple-review-bot

maple-review-bot Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 3/5 · needs attention
The review pass ended early: packages/browser/src/server.ts, failures.ts, navigation.ts, errors.ts, route.ts and index.ts were never read.
quality 100/100 · no findings · tests covered · risk medium · 2/2 new units observable

Warning

This review ended early; what follows is what it established.

Adds server-side trace joining to the browser SDK: a shared traceparent parser, a withMapleProxy middleware wrapper, and Next.js App Router client/server entries. The hunks I read are self-consistent with their tests; six source files went unread before the pass ended.

  • withMapleProxy hands the middleware span's traceparent to the render and to the browser via Server-Timing
  • traceparent.ts parses and writes W3C traceparent without the global propagator
  • nextjs/index.ts ends navigation spans from a root-layout effect and reports boundary errors
What was checked
  • withMapleProxy pass-through cases (no active span, proxy's own response, cross-origin rewrite) all return before any header is written, per server.ts:27,34,63-67
  • renderReceivesTraceparent and forwardToRender agree on x-middleware-override-headers names, and the added tests assert the render-visible header set
  • parseTraceparent rejects version ff, extra fields on version 00 and all-zero ids in traceparent.ts:11,19
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
withMapleProxy Next.js proxy/middleware wrapper inbound entrypoint (middleware) yes Reads and forwards the host's Next.js middleware span context via activeTraceparent() (nextjs/server.ts:25); creates no span of its own by design.
traceparent parse/write helpers library helper yes packages/browser/src/traceparent.ts propagates context rather than instrumenting a call.

3c87b10 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant